﻿// FluentViewModel (http://fluentviewmodel.codeplex.com/)
//
// Copyright © 2010 Kialco Group 
//
// The contents of this file are subject to the Mozilla Public
// License Version 1.1 (the "License"); you may not use this file
// except in compliance with the License. You may obtain a copy of
// the License at http://www.mozilla.org/MPL/
// 
// Software distributed under the License is distributed on an 
// "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
// implied. See the License for the specific language governing
// rights and limitations under the License.

using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System;
using System.Reflection;
using System.Text;
using FluentValidation;
using FluentValidation.Internal;
using FluentValidation.Results;

namespace FluentViewModel
{
    public class ViewModel : IViewModel, INotifyPropertyChanged, IDataErrorInfo
    {
        private readonly IDictionary<string, object> properties;
        private IViewModelConfiguration configuration;
        private object controller;
        private object validator;
        private Type validationContextType;
        private MethodInfo validateObject;
        private MethodInfo validateProperty;

        public ViewModel()
        {
            properties = new Dictionary<string, object>();
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public string this[string columnName]
        {
            get
            {
                return RunValidator(columnName);
            }
        }

        public string Error
        {
            get
            {
                return RunValidator(string.Empty);
            }
        }

        public void Initialize(IViewModelConfiguration viewModelConfiguration)
        {
            configuration = viewModelConfiguration;
            RunInitializers();
            BindController();
            BindCommands();
            BindValidator();
            RunControllerInitialize();
        }

        public void PropertySet(string propertyName, object value)
        {
            properties[propertyName] = value;
            if (configuration == null)
                return;
            RaisePropertyChanged(propertyName);
        }

        public object PropertyGet(Type returnType, string propertyName)
        {
            object value;
            properties.TryGetValue(propertyName, out value);
            return value ?? returnType.GetDefaultValue();
        }

        private void RaisePropertyChanged(string propertyName)
        {
            if (!configuration.Notifiers.Any(x => x == propertyName) &&
                !configuration.Commands.Any(x => x == propertyName))
                return;
            var handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }

        private void RunInitializers()
        {
            foreach (var initializer in configuration.Initializers)
                initializer(this);
        }

        private void BindController()
        {
            controller = configuration
                .ServiceLocator
                .Resolve(configuration.ControllerType);
            if (controller == null)
                throw new InvalidOperationException("Unable to consturct controller.");
        }

        private void BindCommands()
        {
            foreach (var command in configuration.Commands)
                BindCommand(command);
        }

        private void BindCommand(string command)
        {
            var handler = configuration.ControllerType.GetMethod(command, configuration.ViewModelType);
            if (handler != null)
                PropertySet(command, new RelayCommand(() => controller.Invoke(handler, this)));
        }

        private void BindValidator()
        {
            if (configuration.ValidatorType == null)
                return;
            validationContextType = typeof(ValidationContext<>).MakeGenericType(configuration.ViewModelType);
            validator = Activator.CreateInstance(configuration.ValidatorType);
            validateObject = configuration.ValidatorType.GetMethod("Validate", configuration.ViewModelType);
            validateProperty = configuration.ValidatorType.GetMethod("Validate", validationContextType);
        }

        private void RunControllerInitialize()
        {
            var initialize = configuration.ControllerType.GetMethod("Initialize", configuration.ViewModelType);
            controller.Invoke(initialize, this);
        }

        private string RunValidator(string propertyName)
        {
            if (validator == null)
                return string.Empty;
            var result = string.IsNullOrEmpty(propertyName) 
                ? ValidateObject() 
                : ValidateProperty(propertyName);
            return BuildValidationErrorMessage(result);
        }

        private ValidationResult ValidateObject()
        {
            return validator.Invoke<ValidationResult>(validateObject, this);
        }

        private ValidationResult ValidateProperty(string propertyName)
        {
            var context = Activator.CreateInstance(validationContextType, new object[] { this, new PropertyChain(), new MemberNameValidatorSelector(new[] { propertyName }) });
            return validator.Invoke<ValidationResult>(validateProperty, context);
        }

        private static string BuildValidationErrorMessage(ValidationResult result)
        {
            if (result == null)
                return string.Empty;
            var errorMessage = new StringBuilder();
            foreach (var error in result.Errors)
                errorMessage.AppendLine(error.ErrorMessage);
            return errorMessage.ToString();
        }
    }
}